package com.sdy.mvc.feign;

import com.sdy.common.model.BizException;
import com.sdy.common.model.Response;
import com.sdy.common.utils.EncodeUtil;
import com.sdy.common.utils.StringUtil;
import com.sdy.mvc.annotation.RemoteService;
import com.sdy.mvc.config.SdyMvcConfig;
import com.sdy.mvc.utils.AopTargetUtils;
import com.sdy.mvc.utils.HttpUtil;
import com.sdy.mvc.utils.JsonUtil;
import javafx.util.Pair;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ApplicationContext;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class RpcServlet extends HttpServlet {
    private ApplicationContext applicationContext;
    private Map<String, HandlerMethod> handlerMap = new ConcurrentHashMap<>(32);
    private String innerToken;
    public RpcServlet(ApplicationContext applicationContext, String innerToken) {
        this.applicationContext = applicationContext;
        this.innerToken = innerToken;
    }

    @Override
    public void init() throws ServletException {
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(FeignClient.class);
        Set<Object> itfs = new HashSet<>();
        Set<Pair<Object, Object>> impls = new HashSet<>();
        for (Map.Entry<String, Object> entry : beans.entrySet()) {
            Object bean = entry.getValue();
            Object originObject;
            try {
                originObject = AopTargetUtils.getTarget(bean);
            } catch (Exception e) {
                throw new RuntimeException("获取CGLIB动态代理对象失败", e);
            }
            if (originObject.getClass().isAnnotationPresent(RemoteService.class)) {
                impls.add(new Pair<>(bean, originObject));
            } else {
                itfs.add(bean);
            }
        }
        for (Pair<Object, Object> impl : impls) {
            Optional<Class<?>> itf = Arrays.stream(impl.getValue().getClass().getInterfaces())
                    .filter(item -> item.isAnnotationPresent(FeignClient.class))
                    .findFirst();
            if (itf.isPresent()) {
                for (Method method : itf.get().getMethods()) {
                    String mappingUrl = RpcBodyContract.generateMappingUrl(method);
                    handlerMap.put(mappingUrl, new HandlerMethod()
                            .setBean(impl.getKey())
                            .setMethod(method)
                            .setParameters(method.getParameters()));
                }
            }
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = java.net.URLDecoder.decode(req.getRequestURI(), "UTF-8");
        HandlerMethod handlerMethod = handlerMap.get(requestURI);
        if (handlerMethod != null) {
            long t1 = System.currentTimeMillis();
            resp.setStatus(200);
            resp.setCharacterEncoding("UTF-8");
            resp.setContentType("application/json;charset=UTF-8");
            if (doCheckRpcAuth(req, resp)) {
                try (PrintWriter out = resp.getWriter()) {
                    try {
                        Object[] params = getParameterFromRequestBody(req, handlerMethod.getParameters());
                        Object result = handlerMethod.getMethod().invoke(handlerMethod.getBean(), params);
                        byte[] data = JsonUtil.serialize(Response.success(result));
                        out.write(Base64.getEncoder().encodeToString(data));
                    } catch (InvocationTargetException e) {
                        Response<Object> r = Response.error(e.getTargetException().getMessage(), -554);
                        r.setData(e.getTargetException().getClass().getCanonicalName());
                        if (e.getTargetException() instanceof BizException) {
                            log.error("Rpc biz exception:{}", e.getTargetException().getMessage());
                        } else {
                            log.error("Rpc runtime exception:", e.getTargetException());
                        }
                        resp.setStatus(799);
                        byte[] data = JsonUtil.serialize(r);
                        out.write(Base64.getEncoder().encodeToString(data));
                    } catch (Exception e) {
                        log.error("Rpc unchecked exception:", e);
                        Response<Object> r = Response.error(e.getMessage(), -554);
                        r.setData(e.getClass().getCanonicalName());
                        byte[] data = JsonUtil.serialize(r);
                        out.write(Base64.getEncoder().encodeToString(data));
                        resp.setStatus(799);
                    }
                    out.flush();
                }
                log.info("Rpc processing url:[{}] from:{} cost:{}ms", requestURI, HttpUtil.getIpAddr(req), System.currentTimeMillis() - t1);
            } else {
                log.error("Rpc processing url:[{}] from:{} - no auth", requestURI, HttpUtil.getIpAddr(req));
            }
        } else {
            log.error("Rpc processing -- no handler: [{}] from:{}", requestURI, HttpUtil.getIpAddr(req));
        }
    }
    
    private Object[] getParameterFromRequestBody(HttpServletRequest req, Parameter[] parameters) {
        byte[] data = readBodyStreamAsBytes(req);
        RemoteBody remoteBody = JsonUtil.deserialize(data, RemoteBody.class);
        if (remoteBody == null) {
            return new Object[]{};
        }
        Map<Object, Object> map = remoteBody.getParamMap();
        Object[] params = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            params[i] = map.get(String.valueOf(i));
        }
        return params;
    }

    private byte[] readBodyStreamAsBytes(HttpServletRequest request) {
        int len = request.getContentLength();
        byte[] data = new byte[0];

        try (ServletInputStream in = request.getInputStream();
             ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[len];
            int n;
            while (-1 != (n = in.read(buffer))) {
                output.write(buffer, 0, n);
            }
            data = output.toByteArray();
        } catch (IOException e) {
            log.error("Read request body bytes error!", e);
        }
        return data;
    }

    private boolean doCheckRpcAuth(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String innerTokenEnc = request.getHeader(SdyMvcConfig.SERVICE_REQ_TOKEN_ENC);
        if (innerTokenEnc != null) {
            String[] tks = innerTokenEnc.split("#");
            if (tks.length == 3) {
                String enc = tks[0];
                long timestamp = Long.valueOf(tks[2]);
                if (Math.abs(timestamp - System.currentTimeMillis()) > 1000 * 180) {
                    retFailed("Sync time please!", -550, response);
                    return false;
                }
                String enc2 = EncodeUtil.sha256(innerToken.concat("#").concat(tks[1]).concat("#").concat(tks[2]));
                if (StringUtil.isNotBlank(enc) && enc.equals(enc2)) {
                    return true;
                }
            }
        }
        retFailed("Invalid method!", -552, response);
        return false;
    }

    private void retFailed(String msg, Integer code, HttpServletResponse response) throws IOException {
        String respJson = JsonUtil.toJson(Response.error(msg, code));
        try (Writer writer = response.getWriter()) {
            writer.write(respJson);
            writer.flush();
        }
    }
    
    @Data
    @Accessors(chain = true)
    private class HandlerMethod {
        private Method method;
        private Object bean;
        private Parameter[] parameters;
    }
}
